/*
 * Copyright (c) 2010-2011, Yawning <yawninglol at gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   * Neither the name of the Mew Developers nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

package yawning.mew.pivot.feraldruid;

import java.io.IOException;
import java.util.Properties;

import org.apache.pivot.beans.BXML;
import org.apache.pivot.beans.BXMLSerializer;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.HashMap;
import org.apache.pivot.collections.LinkedList;
import org.apache.pivot.collections.Map;
import org.apache.pivot.json.JSON;
import org.apache.pivot.serialization.SerializationException;
import org.apache.pivot.util.concurrent.AbortException;
import org.apache.pivot.util.concurrent.Task;
import org.apache.pivot.util.concurrent.TaskExecutionException;
import org.apache.pivot.util.concurrent.TaskListener;
import org.apache.pivot.wtk.Accordion;
import org.apache.pivot.wtk.ApplicationContext;
import org.apache.pivot.wtk.Border;
import org.apache.pivot.wtk.Button;
import org.apache.pivot.wtk.ButtonPressListener;
import org.apache.pivot.wtk.Component;
import org.apache.pivot.wtk.Label;
import org.apache.pivot.wtk.Meter;
import org.apache.pivot.wtk.PushButton;
import org.apache.pivot.wtk.TaskAdapter;
import org.apache.pivot.wtk.TextArea;

import yawning.mew.Formulation;
import yawning.mew.Mew;
import yawning.mew.MewView;
import yawning.mew.PropertiesBase;
import yawning.mew.Report;
import yawning.mew.Report.Stat;
import yawning.mew.cat.CatModelSimulation;
import yawning.mew.cat.CatSimReport;
import yawning.mew.character.Clazz;
import yawning.mew.character.StatLogic;
import yawning.mew.character.PropertiesToonBuilder;
import yawning.mew.character.TalentTree;
import yawning.mew.character.Toon;
import yawning.mew.pivot.CharacterImport;
import yawning.mew.pivot.MewPivot;
import yawning.mew.pivot.MewPivotBuffsDebuffs;
import yawning.mew.pivot.MewPivotGearBase;
import yawning.mew.pivot.MewPivotModel;
import yawning.mew.pivot.MewPivotRSV;
import yawning.mew.pivot.MewPivotTalentBase;
import yawning.mew.sim.Simulation;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.target.Target;
import yawning.mew.util.CSVDumper;

/**
 * Mew Pivot Feral Druid specific UI components.
 *
 * @author Yawning <yawninglol at gmail.com>
 */
public class FeralDruid extends MewPivotModel implements MewView {
	private CalculateTask mCalculateTask = null;
	private Mew mew = null;
	private PropertiesBase mConfig = null;
	private String mModelOutput = null;
	private StringBuffer mCombatLogOutput = null;
	private StringBuffer mReportOutput = null;

	@BXML private Accordion statsAccordion;
	@BXML private Label modelProgressLabel;
	@BXML private Meter modelProgressMeter;
	@BXML private PushButton calculateButton;
	@BXML private TextArea modelOutput;
	@BXML private TextArea combatLogText;
	@BXML private TextArea reportText;

	private MewPivotRSV rsv;

	private CharacterImport characterImport = null;
	private Gear gear = null;
	private MewPivotBuffsDebuffs buffsDebuffs = null;
	private Talents talents = null;
	private Model modelProperties = null;

	private StringBuilder mLogAccumilator;
	private StringBuilder mErrorAccumilator;
	private StringBuilder mWarningAccumilator;
	private boolean mShouldAccumilateLogs = false;
	private boolean mShouldGenerateRsv = true;

	private long mCurrentIteration;
	private long mPerStatIterations;
	private long mTotalIterations;
	private long mLastQueueTime;
	private static long THROTTLE_PROGRESS_METER_MS = 100;

	private static ArrayList<String> SELF_BUFFS = new ArrayList<String>("Buff.Crit", "Buff.StatMult");
	private static ArrayList<String> SELF_DEBUFFS = new ArrayList<String>("Debuff.Armor", "Debuff.BleedDmg");

	public FeralDruid(MewPivot mewPivot) {
		super(mewPivot);

		mTabName = "Druid (Feral)";

		// Set up the backend.
		mew = new Mew(this);

		// Setup the ui.
		BXMLSerializer bxmlSerializer = new BXMLSerializer();
		try {
			mTab = (Component) bxmlSerializer.readObject(getClass().getResource("FeralDruid.bxml"));
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SerializationException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		bxmlSerializer.bind(this);

		characterImport = new CharacterImport(this);
		statsAccordion.getPanels().add(characterImport.getComponent());
		gear = new Gear();
		statsAccordion.getPanels().add(gear.getComponent());
		talents = new Talents();
		statsAccordion.getPanels().add(talents.getComponent());
		buffsDebuffs = new MewPivotBuffsDebuffs(SELF_BUFFS, SELF_DEBUFFS);
		statsAccordion.getPanels().add(buffsDebuffs.getComponent());
		modelProperties = new Model();
		statsAccordion.getPanels().add(modelProperties.getComponent());
		rsv = new MewPivotRSV();
		((Border) bxmlSerializer.getNamespace().get("rsvTab")).setContent(rsv.getComponent());

		// Wire up the main button.
		calculateButton.getButtonPressListeners().add(
				new ButtonPressListener() {
					@Override
					public void buttonPressed(Button button) {
						onCalculate();
					}
				});

		// Put the progress bar in a sane state.
		updateProgressBar();
	}

	/**
	 * Calculate task.
	 */
	public class CalculateTask extends Task<Void> {
		@Override
		public Void execute() throws TaskExecutionException {
			Toon baseDruid = new PropertiesToonBuilder().setConfig(mConfig).build();
	        Target target = new Target(mConfig, baseDruid);
			StatLogic statLogic = StatLogic.get(baseDruid.level, Clazz.DRUID);
			Properties stats;

			mCombatLogOutput = new StringBuffer();
			mReportOutput = new StringBuffer(mConfig.toString());

			Report result;
			int statScale = Integer.parseInt(mConfig.getProperties().getProperty("Pivot.StatScale"));
			if (statScale == 0) {
				// Attempt to use sane defaults for statScale.
				try {
					Class<?> c = Class.forName(mConfig.getProperties().getProperty("Mew.Class"));
					if (Formulation.class.isAssignableFrom(c)) {
						statScale = 1;
					} else if (Simulation.class.isAssignableFrom(c)) {
						statScale = 100;
//						statScale = (int) Math.ceil(statLogic.getCritConv()); // This may be better.
					} else
						throw new RuntimeException();
				} catch (ClassNotFoundException e) {
					throw new RuntimeException(e);
				}
			}

			mShouldAccumilateLogs = true;

			//
			// Do UI side parameter checking.
			//
			if (!validateStats()) {
				onAbort();
			}

			long startTime = System.nanoTime();

			// Figure out what stat we want to use as the baseline.
			Stat reportStat;
			String reportStatName;
			String pawnModelName;
			boolean isBearModel = false;
			boolean normalizeOutput = false;
			boolean skipWeaponDPS = false;
			if (mConfig.getProperties().getProperty("Mew.Class").equals("yawning.mew.cat.CatModelSimulation")) {
				reportStat = Stat.DAMAGE_DEALT_PER_SECOND;
				reportStatName = "DPS";
				pawnModelName = "Cat";
			} else if (mConfig.getProperties().getProperty("Mew.Class").equals("yawning.mew.bear.BearMitigationFormulation")) {
				if (mConfig.getProperties().getProperty("BearMitigationFormulation.ReportTTL", "false").equals("true")) {
					reportStat = Stat.TIME_TO_LIVE;
					reportStatName = "TTL";
					pawnModelName = "Bear TTL";
				} else {
					reportStat = Stat.MITIGATION_VALUE;
					reportStatName = "Mitigation";
					pawnModelName = "Bear Mitigation";
				}
				isBearModel = true;
				normalizeOutput = true;
			} else if (mConfig.getProperties().getProperty("Mew.Class").equals("yawning.mew.bear.BearThreatSimulation")) {
				if (mConfig.getProperties().getProperty("Simulator.ReportMitigation", "false").equals("true")) {
					reportStat = Stat.MITIGATION_VALUE;
					reportStatName = "Mitigation";
					pawnModelName = "Bear Mitigation";
					normalizeOutput = true;
					skipWeaponDPS = true;
				} else {
					reportStat = Stat.THREAT_PER_SECOND;
					reportStatName = "TPS";
					pawnModelName = "Bear TPS";
				}
				isBearModel = true;
			} else {
				throw new RuntimeException();
			}

			//
			// Calculate the baseline (not always DPS).
			//
			double baseDPS = 0, baseError;
			if ((result = mew.compute(baseDruid, target, mConfig)) == null || abort) {
				onAbort();
			}
			baseDPS = result.getResult(reportStat).getMean();
			baseError = result.getResult(reportStat).getStandardError();

			//
			// Store the information we will end up displaying in the report
			// pane now.
			//
			mShouldAccumilateLogs = false;

			StringBuilder tmp = new StringBuilder();
			String warnings = mWarningAccumilator.toString();
			String logs = mLogAccumilator.toString();
			String report = result.toString();

			if (warnings.length() > 0) {
				tmp.append(warnings);
				tmp.append('\n');
			}
			if (logs.length() > 0) {
				tmp.append(logs);
				tmp.append('\n');
			}
			if (report.length() > 0) {
				tmp.append(report);
				tmp.append('\n');
			}

			mModelOutput = tmp.toString();
			mReportOutput.append("\n\n").append(tmp);

			if (result.getLog() != null) {
				mCombatLogOutput.append(result.getLog());
			} else {
				mCombatLogOutput.append("Combat Log Generation is disabled or not supported for this model.\n");
			}

			// FIXME: Ew.  This isn't all that clean.
			if (CatSimReport.class.isInstance(result)) {
				CatSimReport catReport = (CatSimReport) result;
				if (mConfig.getBool(CatModelSimulation.SimulatorProperty.LOGDAMAGE)) {
					CSVDumper.dump(mConfig.getProperties().getProperty("Pivot.CSVFile"), "Damage Dump:\nConfig: " + mConfig, catReport.mDamageLogs);
				}
			}

			if (!mShouldGenerateRsv) {
				rsv.clearRsvTableData();
				double elapsed = (double)(System.nanoTime() - startTime) / 1000000000;
				mReportOutput.append("\nElapsed Time: " + elapsed + " seconds\n");
				return null;
			}

			//
			// Generate relative stat values now.
			//

			// Don't need to generate logging for these.
			mConfig.setBool(Property.SIMULATION_GENERATE_LOG, false);
			mConfig.setBool(CatModelSimulation.SimulatorProperty.LOGDAMAGE, false);

			if (isBearModel) {
				// Bears care about Stamina, Armor, Bonus Armor and Dodge Rating. (XXX: Exp to hard cap?)
				mTotalIterations += 4 * mPerStatIterations;
				if (skipWeaponDPS) 	mTotalIterations -= mPerStatIterations;	// Skip weapon DPS.
			}

			// Calculate Stamina DPS.
			double staminaDPS = 0;
			double staminaError = 0;
			if (isBearModel) {
				stats = mConfig.copyProperties();
				stats.setProperty("Stamina", Integer.toString(baseDruid.stamina + statScale));
				if ((result = mew.compute(new PropertiesToonBuilder() .setProperties(stats).build(), target, mConfig)) == null || abort) {
					onAbort();
				}
				staminaDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
				staminaError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;
			}

			// Calculate armor DPS.
			double armorDPS = 0;
			double armorError = 0;
			if (isBearModel) {
				stats = mConfig.copyProperties();
				stats.setProperty("Armor", Integer.toString(baseDruid.armor + statScale));
				if ((result = mew.compute(new PropertiesToonBuilder() .setProperties(stats).build(), target, mConfig)) == null || abort) {
					onAbort();
				}
				armorDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
				armorError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;
			}

			// Calculate bonus armor DPS.
			double bonusArmorDPS = 0;
			double bonusArmorError = 0;
			if (isBearModel) {
				stats = mConfig.copyProperties();
				stats.setProperty("BonusArmor", Integer.toString(baseDruid.bonusArmor + statScale));
				if ((result = mew.compute(new PropertiesToonBuilder() .setProperties(stats).build(), target, mConfig)) == null || abort) {
					onAbort();
				}
				bonusArmorDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
				bonusArmorError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;
			}

			// Calculate Strength DPS.
//			double strengthDPS = 0;
//			stats = mConfig.copyProperties();
//			stats.setProperty("Strength",
//					Integer.toString(baseDruid.strength + statScale));
//			if ((result = mew.compute(new PropertiesToonBuilder()
//					.setProperties(stats).build(), target, mConfig)) == null
//					|| abort) {
//				onAbort();
//			}
//			strengthDPS = (result.getResult(reportStat).getMean() - baseDPS)
//					/ statScale;
//			double strengthError = propogateDiffError(baseError,
//					result.getResult(reportStat).getStandardError())
//					/ statScale;

			// Calculate Agility DPS.
			double agilityDPS = 0;
			stats = mConfig.copyProperties();
			stats.setProperty("Agility", Integer.toString(baseDruid.agility + statScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			agilityDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
			double agilityError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;

			// Calculate AP DPS.
			double apDPS = 0;
			stats = mConfig.copyProperties();
			stats.setProperty("AttackPower", Integer.toString(baseDruid.attackPower + statScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			apDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
			double apError = propogateDiffError(baseError,	result.getResult(reportStat).getStandardError()) / statScale;

			// Calculate Strength DPS.
			// Note: Since this is a static boost, just cheat and derive it based off apDPS.
			double strengthDPS = apDPS * (stats.getProperty("Buff.StatMult", "false").equals("true") ? 1.05 : 1);
			double strengthError = apError;

			// Calculate Haste rating DPS.
			double hasteDPS = 0;
			stats = mConfig.copyProperties();
			stats.setProperty("HasteRating", Integer.toString(baseDruid.hasteRating + statScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			hasteDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
			double hasteError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError())	/ statScale;

			// Calculate Crit rating DPS.
			double critDPS = 0;
			stats = mConfig.copyProperties();
			stats.setProperty("CritRating",	Integer.toString(baseDruid.critRating + statScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			critDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
			double critError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;

			// Calculate Mastery rating DPS.
			double masteryDPS = 0;
			stats = mConfig.copyProperties();
			stats.setProperty("MasteryRating", Integer.toString(baseDruid.masteryRating + statScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null
					|| abort) {
				onAbort();
			}
			masteryDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
			double masteryError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;

			// Calculate Hit rating DPS.
			double hitDPS = 0, hitError;
			stats = mConfig.copyProperties();
			int hitStatScale = statLogic.getMeleeSpecialHitRatingCap(target.getLevel());

			double hitDPSHigh = 0;
			stats.setProperty("HitRating", Integer.toString(hitStatScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			hitDPSHigh = result.getResult(reportStat).getMean();
			double hitErrorHigh = result.getResult(reportStat).getStandardError();

			double hitDPSLow = 0;
			stats.setProperty("HitRating", Integer.toString(0));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			hitDPSLow = result.getResult(reportStat).getMean();
			double hitErrorLow = result.getResult(reportStat).getStandardError();

			hitDPS = (hitDPSHigh - hitDPSLow) / hitStatScale;
			hitError = propogateDiffError(hitErrorHigh, hitErrorLow) / hitStatScale;

			// Calculate Expertise DPS.
			double expertiseDPS = 0, expertiseError;
			stats = mConfig.copyProperties();
			int expertiseStatScale = statLogic.getExpertiseRatingSoftCap(target.getLevel());

			double expertiseDPSHigh = 0;
			stats.setProperty("ExpertiseRating", Integer.toString(expertiseStatScale));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			expertiseDPSHigh = result.getResult(reportStat).getMean();
			double expertiseErrorHigh = result.getResult(reportStat).getStandardError();

			double expertiseDPSLow = 0;
			stats.setProperty("ExpertiseRating", Integer.toString(0));
			if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
				onAbort();
			}
			expertiseDPSLow = result.getResult(reportStat).getMean();
			double expertiseErrorLow = result.getResult(reportStat).getStandardError();

			expertiseDPS = (expertiseDPSHigh - expertiseDPSLow) / expertiseStatScale;
			expertiseError = propogateDiffError(expertiseErrorHigh, expertiseErrorLow) / expertiseStatScale;

			// Calculate Dodge Rating DPS.
			double dodgeDPS = 0;
			double dodgeError = 0;
			if (isBearModel) {
				stats = mConfig.copyProperties();
				stats.setProperty("DodgeRating", Integer.toString(baseDruid.mDodgeRating + statScale));
				if ((result = mew.compute(new PropertiesToonBuilder() .setProperties(stats).build(), target, mConfig)) == null || abort) {
					onAbort();
				}
				dodgeDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
				dodgeError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;
			}

			// Calculate Weapon DPS.
			double weaponDPS = 0;
			double weaponError = 0;
			if (!skipWeaponDPS) {
				stats = mConfig.copyProperties();
				double weaponSpeed = baseDruid.weaponSpeed / 1000;
				stats.setProperty("WeaponTop", Integer.toString((int) Math.floor(baseDruid.weaponTop + statScale * weaponSpeed)));
				stats.setProperty("WeaponBottom", Integer.toString((int) Math.floor(baseDruid.weaponBottom + statScale * weaponSpeed)));
				if ((result = mew.compute(new PropertiesToonBuilder().setProperties(stats).build(), target, mConfig)) == null || abort) {
					onAbort();
				}
				weaponDPS = (result.getResult(reportStat).getMean() - baseDPS) / statScale;
				weaponError = propogateDiffError(baseError, result.getResult(reportStat).getStandardError()) / statScale;
			}

			//
			// All mitigation models produces values that are tiny so normalize them around Agi.
			//

			if (normalizeOutput) {
				staminaDPS = staminaDPS / agilityDPS;
				armorDPS = armorDPS / agilityDPS;
				bonusArmorDPS = bonusArmorDPS / agilityDPS;
				strengthDPS = strengthDPS / agilityDPS;
				apDPS = apDPS / agilityDPS;
				critDPS = critDPS / agilityDPS;
				hasteDPS = hasteDPS / agilityDPS;
				masteryDPS = masteryDPS / agilityDPS;
				hitDPS = hitDPS / agilityDPS;
				expertiseDPS = expertiseDPS / agilityDPS;
				dodgeDPS = dodgeDPS / agilityDPS;
				weaponDPS = weaponDPS / agilityDPS;

				// XXX: Is this correct?
				if (agilityError > 0) {
					staminaError = staminaError / agilityDPS;
					armorError = armorError / agilityDPS;
					bonusArmorError = bonusArmorError / agilityDPS;
					strengthError = strengthError / agilityDPS;
					apError = apError / agilityDPS;
					critError = critError / agilityDPS;
					hasteError = hasteError / agilityDPS;
					masteryError = masteryError / agilityDPS;
					hitError = hitError / agilityDPS;
					expertiseError = expertiseError / agilityDPS;
					dodgeError = dodgeError / agilityDPS;
					weaponError = weaponError / agilityDPS;
					agilityError = agilityError / agilityDPS;
				}

				agilityDPS = 1.0f;
			}

			//
			// Generate a pawn scale.
			//
			// Notes:
			// * I explicitly filter out usable but suboptimal item types. (No, I do not care about cloth or 1h weapons.)
			// * I should set "MetaGemEffect", but using a crit multiplier meta should be common sense.
			//
			StringBuilder pawnBuilder = new StringBuilder();
			pawnBuilder.append("( Pawn: v1: \"Feral Druid ").append(pawnModelName).append(" (Mew)\":");
			if (isBearModel) {
				pawnBuilder.append(" Stamina=").append(Report.round(staminaDPS)).append(',');
				pawnBuilder.append(" Armor=").append(Report.round(armorDPS)).append(',');
				pawnBuilder.append(" BonusArmor=").append(Report.round(bonusArmorDPS)).append(',');
			}
			pawnBuilder.append(" Strength=").append(Report.round(strengthDPS)).append(',');
			pawnBuilder.append(" Agility=").append(Report.round(agilityDPS)).append(',');
			pawnBuilder.append(" Ap=").append(Report.round(apDPS)).append(',');
			pawnBuilder.append(" CritRating=").append(Report.round(critDPS)).append(',');
			pawnBuilder.append(" HasteRating=").append(Report.round(hasteDPS)).append(',');
			pawnBuilder.append(" MasteryRating=").append(Report.round(masteryDPS)).append(',');
			pawnBuilder.append(" HitRating=").append(Report.round(hitDPS)).append(',');
			pawnBuilder.append(" ExpertiseRating=").append(Report.round(expertiseDPS)).append(',');
			if (isBearModel) {
				pawnBuilder.append(" DodgeRating=").append(Report.round(dodgeDPS)).append(',');
			}
			pawnBuilder.append(" MeleeDps=").append(Report.round(weaponDPS)).append(',');
			pawnBuilder.append(" IsWand=-1000000, IsCrossbow=-1000000, IsGun=-1000000, Is2HAxe=-1000000, IsThrown=-1000000,  IsBow=-1000000, IsSword=-1000000, Is2HSword=-1000000, IsAxe=-1000000, IsShield=-1000000, IsDagger=-1000000, IsFist=-1000000, IsMace=-1000000,");
			// pawnBuilder.append(" IsPlate=-1000000, IsMail=-1000000, IsCloth=-1000000");
			pawnBuilder.append(" IsPlate=-1000000, IsMail=-1000000,");
			pawnBuilder.append(" SpellPower=-1000000, Intellect=-1000000");
			pawnBuilder.append(" )");

			rsv.setPawnString(pawnBuilder.toString());

			double elapsed = (double)(System.nanoTime() - startTime) / 1000000000;
			mReportOutput.append("\nElapsed Time: ").append(elapsed).append(" seconds\n");
			mReportOutput.append("\nRelative Stat Values:\n\n");

			//
			// Generate the RSV output now.
			//
			LinkedList<Object> rsvTableData = new LinkedList<Object>();
			HashMap<String, Object> rsvData;

			if (isBearModel) {
				rsvData = new HashMap<String, Object>();
				JSON.put(rsvData, "stat", "Stamina");
				JSON.put(rsvData, "dps", staminaDPS);
				JSON.put(rsvData, "error", staminaError * 1.96);
				rsvTableData.add(rsvData);
				mReportOutput.append("Stamina: ").append(staminaDPS).append(" +/- ").append(staminaError * 1.96).append('\n');

				rsvData = new HashMap<String, Object>();
				JSON.put(rsvData, "stat", "Armor");
				JSON.put(rsvData, "dps", armorDPS);
				JSON.put(rsvData, "error", armorError * 1.96);
				rsvTableData.add(rsvData);
				mReportOutput.append("Armor: ").append(armorDPS).append(" +/- ").append(armorError * 1.96).append('\n');

				rsvData = new HashMap<String, Object>();
				JSON.put(rsvData, "stat", "Bonus Armor");
				JSON.put(rsvData, "dps", bonusArmorDPS);
				JSON.put(rsvData, "error", bonusArmorError * 1.96);
				rsvTableData.add(rsvData);
				mReportOutput.append("Bonus Armor: ").append(bonusArmorDPS).append(" +/- ").append(bonusArmorError * 1.96).append('\n');
			}

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Strength");
			JSON.put(rsvData, "dps", strengthDPS);
			JSON.put(rsvData, "error", strengthError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Strength: ").append(strengthDPS).append(" +/- ").append(strengthError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Agility");
			JSON.put(rsvData, "dps", agilityDPS);
			JSON.put(rsvData, "error", agilityError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Agility: ").append(agilityDPS).append(" +/- ").append(agilityError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Attack Power");
			JSON.put(rsvData, "dps", apDPS);
			JSON.put(rsvData, "error", apError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Attack Power: ").append(apDPS).append(" +/- ").append(apError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Haste Rating");
			JSON.put(rsvData, "dps", hasteDPS);
			JSON.put(rsvData, "error", hasteError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Haste Rating: ").append(hasteDPS).append(" +/- ").append(hasteError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Crit Rating");
			JSON.put(rsvData, "dps", critDPS);
			JSON.put(rsvData, "error", critError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Crit Rating: ").append(critDPS).append(" +/- ").append(critError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Mastery Rating");
			JSON.put(rsvData, "dps", masteryDPS);
			JSON.put(rsvData, "error", masteryError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Mastery Rating: ").append(masteryDPS).append(" +/- ").append(masteryError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Hit Rating");
			JSON.put(rsvData, "dps", hitDPS);
			JSON.put(rsvData, "error", hitError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Hit Rating: ").append(hitDPS).append(" +/- ").append(hitError * 1.96).append('\n');

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Expertise Rating");
			JSON.put(rsvData, "dps", expertiseDPS);
			JSON.put(rsvData, "error", expertiseError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Expertise Rating: ").append(expertiseDPS).append(" +/- ").append(expertiseError * 1.96).append('\n');

			if (isBearModel) {
				rsvData = new HashMap<String, Object>();
				JSON.put(rsvData, "stat", "Dodge Rating");
				JSON.put(rsvData, "dps", dodgeDPS);
				JSON.put(rsvData, "error", dodgeError * 1.96);
				rsvTableData.add(rsvData);
				mReportOutput.append("Dodge Rating: ").append(dodgeDPS).append(" +/- ").append(dodgeError * 1.96).append('\n');
			}

			rsvData = new HashMap<String, Object>();
			JSON.put(rsvData, "stat", "Weapon DPS");
			JSON.put(rsvData, "dps", weaponDPS);
			JSON.put(rsvData, "error", weaponError * 1.96);
			rsvTableData.add(rsvData);
			mReportOutput.append("Weapon DPS: ").append(weaponDPS).append(" +/- ").append(weaponError * 1.96).append('\n');

			rsv.setRsvTableData(rsvTableData, reportStatName);

			return null;
		}

		private double propogateDiffError(double a, double b) {
			return Math.sqrt(a * a + b * b);
		}

		@Override
		public void abort() {
			onError("Calculations terminated by user.");
			super.abort();
			mew.abort();
		}

		private void onAbort() {
			throw new AbortException();
		}
	}

	/**
	 * Calculate button handler.
	 */
	public void onCalculate() {
		if (mCalculateTask == null) {
			Properties mewProps = new Properties();

			mewProps.setProperty("Class", "Druid");

			// Build our properties.
			mewProps.putAll(gear.toProperties());
			mewProps.putAll(buffsDebuffs.toProperties());
			mewProps.putAll(talents.toProperties());
			mewProps.putAll(modelProperties.toProperties());

			mConfig = new PropertiesBase(mewProps);

			// Initialize logging.
			mLogAccumilator = new StringBuilder();
			mWarningAccumilator = new StringBuilder();
			mErrorAccumilator = new StringBuilder();
			mModelOutput = null;
			mReportOutput = null;

			mShouldGenerateRsv = Boolean.parseBoolean(mewProps.getProperty(
					"Pivot.GenerateRsv", "false"));

			// Figure out how many iterations we are going to do.
			mCurrentIteration = 0;
			mPerStatIterations = Integer.parseInt(mewProps.getProperty(
					"Simulator.Iterations", "0"));
			mTotalIterations = (mShouldGenerateRsv) ? mPerStatIterations * 11
					: mPerStatIterations; // 1 pass for base DPS, 8 different stats, 2 extra for hit/exp.

			rsv.clearPawnString();
			rsv.clearRsvTableData();

			// Good to go, spawn our thread here.
			mCalculateTask = new CalculateTask();
			mCalculateTask.execute(new TaskAdapter<Void>(
					new TaskListener<Void>() {
						@Override
						public void executeFailed(Task<Void> task) {
							mModelOutput = mErrorAccumilator.toString();

							reset();
						}

						@Override
						public void taskExecuted(Task<Void> task) {
							reset();
						}

						private void reset() {
							modelOutput.setText(mModelOutput);
							combatLogText.setText(mCombatLogOutput.toString());
							reportText.setText(mReportOutput.toString());

							// Do we really need to do this?
							reportText.selectAll();
							reportText.copy();

							mCalculateTask = null;
							updateProgressBar();
							updateCalculateButton();
						}
					}));
		} else {
			mCalculateTask.abort();
		}

		updateCalculateButton();
		updateProgressBar();
	}

	private boolean validateStats() {
		Properties p = mConfig.getProperties();

		//
		// Check to see if the parameters that were passed in resemble something
		// sane.
		//
		if (p.getProperty("Talent.Leader of the Pack", "0").equals("1")) {
			if (p.getProperty("Buff.Crit", "false").equals("false")) {
				// Hmm, the druid is speced LotP, but isn't setting the Crit
				// Strike Chance buff.
				onError("Buffs/Debuffs:Buffs:Critical Strike Chance should be set. (Leader of the Pack)");
				return false;
			}
		}

		if (p.getProperty("Debuff.BleedDmg", "false").equals("false")) {
			onError("Buffs/Debuffs:Debuffs:Bleed Damage Increase should be set. (Mangle)");
			return false;
		}

		if (p.getProperty("Mew.Class").equals(
				"yawning.mew.cat.CatModelSimulation")) {
			if (p.getProperty("Simulator.AggressiveFeralCharge", "false")
					.equals("true")) {
				if (p.getProperty("Talent.Stampede", "0").equals("0")) {
					onError("Mid-encounter Ravage modeling requested without Stampede.");
					return false;
				}
				if (p.getProperty("Simulator.FeralChargeTravelTime", "0").equals("0")) {
					onError("Mid-encounter Ravage modeling requires non-zero travel time.");
					return false;
				}
			}

			boolean tranq = p.getProperty("Simulator.Tranquility", "false")
					.equals("true");
			boolean rebirth = p.getProperty("Simulator.Rebirth", "false")
					.equals("true");
			int encounterDuration = Integer.parseInt(p.getProperty(
					"Simulator.Duration", "0"));
			if ((tranq || rebirth) && encounterDuration < 30) {
				onError("Rebirth/Tranquility modeling requires Encounter Duration > 30 sec.");
				return false;
			}

			boolean bug_shred_simcraft = p.getProperty("Bug.GlyphOfShredSimCraft", "false").equals("true");
			boolean bug_shred_3sec = p.getProperty("Bug.GlyphOfShred3Sec", "false").equals("true");
			if (bug_shred_simcraft && bug_shred_3sec) {
				onWarn("Glyph of Bloodletting bug models are mutually exclusive, using the 3/3/(2 or 4) sec implementation.");
				p.setProperty("Bug.GlyphOfShredSimCraft", "false");
			}
		}

		if (p.getProperty("Mew.Class").equals("yawning.mew.bear.BearMitigationFormulation")) {
			if (Double.parseDouble(p.getProperty("Target.SwingInterval", "0")) <= 0) {
				onError("Mitigation Formulation requires Mob Swing Interval > 0");
				return false;
			}
			if (Integer.parseInt(p.getProperty("Target.SwingDamage", "0")) <= 0) {
				onError("Mitigation Formulation requires Mob Swing Damage > 0");
				return false;
			}
		}

		if (p.getProperty("Mew.Class").equals("yawning.mew.bear.BearThreatSimulation")) {
			if (p.getProperty("Simulator.UnlimitedRage", "false").equals("false")) {
				boolean noWhiteDamage = Double.parseDouble(p.getProperty("Target.SwingInterval", "0")) <= 0 || Integer.parseInt(p.getProperty("Target.SwingDamage", "0")) <= 0;
				boolean noYellowDamage = Double.parseDouble(p.getProperty("Target.YellowInterval", "0")) <= 0 || Integer.parseInt(p.getProperty("Target.YellowDamage", "0")) <= 0;
				boolean noMagicDamage = Double.parseDouble(p.getProperty("Target.MagicInterval", "0")) <= 0 || Integer.parseInt(p.getProperty("Target.MagicDamage", "0")) <= 0;

				if (noWhiteDamage && noYellowDamage && noMagicDamage) {
					onWarn("Threat simulation calculated with self generated rage only.");
				}
			}
		}

		if (p.getProperty("Simulator.Ultraxion", "false").equals("true")) {
			if (p.getProperty("Simulator.FrontalAttacks", "false").equals("false")) {
				onWarn("'Boss Cannot Parry' setting ignored ('Frontal Attacks' is required for this option to have effect).");
			}
		}

		if (Integer.parseInt(p.getProperty("Pivot.nrKirilPolearms", "0")) > 1) {
			onError("Multiple Kiril, Fury of the Beasts can not be evaluated simultaniously.");
			return false;
		}

		// if (p.getProperty("Buffs.StatMult", "false").equals("false")) {
		// onWarn("Buffs/Debuffs:Buffs:Stat Multiplier should be set. (Mark of the Wild)");
		// }
		//
		// if (p.getProperty("Debuff.Armor", "false").equals("false")) {
		// onWarn("Buffs/Debuffs:Debuffs:Armor should be set. (Feral Farie Fire)");
		// }

		return true;
	}

	private void updateCalculateButton() {
		if (mCalculateTask == null) {
			calculateButton.setButtonData("Calculate");
		} else {
			calculateButton.setButtonData("Cancel");
		}
	}

	private void updateProgressBar() {
		if (mCalculateTask == null) {
			modelProgressLabel.setVisible(false);
			modelProgressMeter.setVisible(false);
			modelProgressMeter.setPercentage(0);
		} else {
			modelProgressLabel.setVisible(true);
			modelProgressMeter.setVisible(true);
		}
	}

	/**
	 * Open handler.
	 */
	@Override
	public void onOpen(Map<String, Object> backingStore) {
		characterImport.onLoad(backingStore);
		gear.onLoad(backingStore);
		talents.onLoad(backingStore);
		buffsDebuffs.onLoad(backingStore);
		modelProperties.onLoad(backingStore);
	}

	/**
	 * Save handler.
	 */
	@Override
	public void onSave(Map<String, Object> backingStore) {
		characterImport.onSave(backingStore);
		gear.onSave(backingStore);
		talents.onSave(backingStore);
		buffsDebuffs.onSave(backingStore);
		modelProperties.onSave(backingStore);
	}

	/**
	 * Close handler.
	 */
	@Override
	public void onClose() {
		// I should cancel in-progress calculations here.
		if (mCalculateTask != null) {
			mCalculateTask.abort();
		}
	}

	//
	// MewView methods.
	//

	@Override
	public void onLog(String s) {
		if (mShouldAccumilateLogs == true) {
			mLogAccumilator.append(s);
			mLogAccumilator.append('\n');
		}
	}

	@Override
	public void onWarn(String s) {
		if (mShouldAccumilateLogs == true) {
			mWarningAccumilator.append("WARNING: ");
			mWarningAccumilator.append(s);
			mWarningAccumilator.append('\n');
		}
	}

	@Override
	public void onError(String s) {
		mErrorAccumilator.append("ERROR: ");
		mErrorAccumilator.append(s);
		mErrorAccumilator.append('\n');
	}

	@Override
	public void onIteration(int iteration, int maxIteration) {
		mCurrentIteration++;

		long currentTime = System.currentTimeMillis();
		if (currentTime - mLastQueueTime >= THROTTLE_PROGRESS_METER_MS) {
			mLastQueueTime = currentTime;
			ApplicationContext.queueCallback(new Runnable() {
				@Override
				public void run() {
					double percent = Math.min(1.0, (double) mCurrentIteration
							/ mTotalIterations);

					modelProgressMeter.setPercentage(percent);
				}
			});
		}
	}

	// Accessors for the armory import code.
	@Override
	public MewPivotGearBase getGearComponent() {
		return gear;
	}

	@Override
	public MewPivotTalentBase getTalentComponent() {
		return talents;
	}

	@Override
	public Clazz getExpectedClazz() {
		return Clazz.DRUID;
	}

	@Override
	public TalentTree getExpectedTalentTree() {
		return TalentTree.get(Clazz.DRUID, "Feral Combat");
	}
}
